package org.ricks.log;

import org.ricks.common.conf.LogConfig;
import org.ricks.utils.StrUtil;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;


public final class RollingFileWriter extends AbstractFormatPatternWriter {

    private final DynamicPath path;
    private final List<Policy> policies;
    private final FileConverter converter;
    private final DynamicPath linkToLatest;
    private final Charset charset = Charset.defaultCharset();

    private ByteArrayWriter writer;

    
    public RollingFileWriter()  {
        super();
        path = new DynamicPath(LogConfig.getFile());
        policies = createPolicies(LogConfig.getPolicies());
        converter = createConverter("");
        linkToLatest = StrUtil.isNotBlank(LogConfig.getLatest()) ? new DynamicPath(LogConfig.getLatest()) : null;

        List<FileTuple> files = getAllFileTuplesWithoutLinks(converter.getBackupSuffix());
        File latestFile = findLatestLogFile(files);

        if(LogConfig.getBackups() >= 0) deleteBackups(files);

        String fileName;
        boolean append;

        if (latestFile != null && path.isValid(latestFile)) {
            fileName = latestFile.getAbsolutePath();
            if (canBeContinued(fileName, policies)) {
                append = true;
            } else {
                fileName = path.resolve();
                append = false;
            }
        } else {
            fileName = path.resolve();
            append = false;
        }

        try {
            writer = createByteArrayWriterAndLinkLatest(fileName, append);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void write(final LogEntry logEntry) throws IOException {
        byte[] data = render(logEntry).getBytes(charset);
        internalWrite(data);
    }

    @Override
    public void flush() throws IOException {
        internalFlush();
    }

    @Override
    public void close() throws IOException, InterruptedException {
        internalClose();
    }

    
    private void internalWrite(final byte[] data) throws IOException {
        if (!canBeContinued(data, policies)) {
            writer.close();
            converter.close();

            String fileName = path.resolve();
            writer = createByteArrayWriterAndLinkLatest(fileName, false);

            for (Policy policy : policies) {
                policy.reset();
            }
        }

        byte[] convertedData = converter.write(data);
        writer.write(convertedData, convertedData.length);
    }

    
    private void internalFlush() throws IOException {
        writer.flush();
    }

    
    private void internalClose() throws IOException, InterruptedException {
        writer.close();
        converter.close();
        converter.shutdown();
    }

    private List<FileTuple> getAllFileTuplesWithoutLinks(final String backupSuffix) {
        List<FileTuple> files = path.getAllFiles(backupSuffix);
        if (linkToLatest != null) {
            File fileLink = new File(linkToLatest.resolve()).getAbsoluteFile();
            Iterator<FileTuple> iterator = files.iterator();
            while (iterator.hasNext()) {
                if (fileLink.equals(iterator.next().getOriginal())) {
                    iterator.remove();
                    break;
                }
            }
        }
        return files;
    }

    
    private ByteArrayWriter createByteArrayWriterAndLinkLatest(final String fileName, final boolean append) throws FileNotFoundException {
        converter.open(fileName);
        ByteArrayWriter writer = createByteArrayWriter(fileName, append);
        if (linkToLatest != null) {
            File logFile = new File(fileName);
            File linkFile = new File(linkToLatest.resolve());
            try {
                Path logPath = logFile.toPath();
                Path linkPath = linkFile.toPath();
                Files.deleteIfExists(linkPath);
                Files.createLink(linkPath, logPath);
            } catch (IOException ex) {
                InternalLogger.log(Level.ERROR, ex, "Failed to create link '" + linkFile + "'");
            }
        }
        return writer;
    }

    
    private static File findLatestLogFile(final List<FileTuple> files) {
        for (FileTuple file : files) {
            if (file.getOriginal().isFile() && (file.getOriginal().equals(file.getBackup()) || !file.getBackup().isFile())) {
                return file.getOriginal();
            }
        }

        return null;
    }

    
    private static List<Policy> createPolicies(final String property) {
        if (property == null || property.isEmpty()) {
            return Collections.<Policy>singletonList(new StartupPolicy(null));
        } else {
            if (RuntimeProvider.getProcessId() == Long.MIN_VALUE) {
                java.util.ServiceLoader.load(Policy.class);
            }

            return new ServiceLoader<Policy>(Policy.class, String.class).createList(property);
        }
    }

    
    private static FileConverter createConverter(final String property) {
        if (property == null || property.isEmpty()) {
            return new NopFileConverter();
        } else {
            if (RuntimeProvider.getProcessId() == Long.MIN_VALUE) {
                java.util.ServiceLoader.load(FileConverter.class);
            }

            FileConverter converter = new ServiceLoader<FileConverter>(FileConverter.class).create(property);
            return converter == null ? new NopFileConverter() : converter;
        }
    }

    
    private static boolean canBeContinued(final String fileName, final List<Policy> policies) {
        boolean result = true;
        for (Policy policy : policies) {
            result &= policy.continueExistingFile(fileName);
        }
        return result;
    }

    
    private static boolean canBeContinued(final byte[] data, final List<Policy> policies) {
        boolean result = true;
        for (Policy policy : policies) {
            result &= policy.continueCurrentFile(data);
        }
        return result;
    }

    
    private static void deleteBackups(final List<FileTuple> files) {
        for (int i = LogConfig.getBackups(); i < files.size(); ++i) {
            files.get(i).delete();
        }
    }

}